feat: parse parameter decorators for transforms#3001
feat: parse parameter decorators for transforms#3001jtenner wants to merge 15 commits intoAssemblyScript:mainfrom
Conversation
|
Whoops! I forgot to check the validation scripts locally. Everything looks good now. I hope this contribution finds it's way to main because I want to use it to make some very cheeky transforms for my webgpu implementation. |
4ec3371 to
4786023
Compare
|
Is this all set? I rebased it on main and had to add the eslint "ignore" file because of the decorator proof. I left a comment to explain why. Please let me know if anything isn't up to standard! |
|
👀 are you planning on using this for WGSL/GLSL translation? |
There was a problem hiding this comment.
Pull request overview
Enables parsing and AST preservation of parameter decorators (including decorators on explicit this) so transforms can inspect/remove them before compilation-time validation, while keeping parameter decorators invalid if they survive past transform time.
Changes:
- Extend the AST (
ParameterNode,FunctionTypeNode) and serializer to round-trip preserved parameter decorators. - Update the parser to accept/preserve parameter decorators across function declarations/expressions, methods, constructors, and function types (including explicit
this). - Add a post-transform validation pass that emits
TS1206for any remaining parameter decorators, plus parser/compiler/transform tests and docs timing updates.
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/ast.ts |
Adds storage for parameter decorators on ParameterNode and explicit-this decorators on FunctionTypeNode. |
src/parser.ts |
Parses and preserves parameter decorators in parameter lists and function types; threads explicit-this decorators through function expression parsing. |
src/extra/ast.ts |
Serializes preserved parameter decorators so they can be round-tripped. |
src/program.ts |
Implements a one-shot AST traversal to report surviving parameter decorators after transforms. |
src/compiler.ts |
Runs the new post-transform parameter-decorator validation after initialization. |
src/index-wasm.ts |
Clarifies initializeProgram timing for transforms relative to validation. |
cli/index.js |
Documents afterInitialize as the last AST rewrite point before compilation-time validation. |
cli/index.d.ts |
Updates transform API docs to clarify afterInitialize timing and intent. |
tests/parser/parameter-decorators.ts |
Adds parser coverage for parameter decorator syntax across constructs. |
tests/parser/parameter-decorators.ts.fixture.ts |
Adds parser fixture inputs covering multiple decorators, this, and rest parameters. |
tests/compiler/parameter-decorators.ts |
Adds compiler test ensuring TS1206 is emitted if decorators survive. |
tests/compiler/parameter-decorators.json |
Expected stderr for surviving parameter decorator validation. |
tests/compiler/parameter-decorators-errors.ts |
Adds invalid-syntax compiler coverage for decorator placement/forms. |
tests/compiler/parameter-decorators-errors.json |
Expected stderr for invalid parameter decorator syntax cases. |
tests/transform/parameter-decorators.ts |
Transform test input program using parameter decorators (including on this). |
tests/transform/remove-parameter-decorators.js |
ESM transform that strips preserved parameter decorators during afterInitialize. |
tests/transform/cjs/remove-parameter-decorators.js |
CommonJS transform that strips preserved parameter decorators during afterInitialize. |
package.json |
Extends transform test scripts to cover decorator stripping behavior in ESM+CJS transforms. |
eslint.config.js |
Ignores the transform fixture that is intentionally invalid TypeScript syntax. |
.eslintrc.cjs |
Adds a legacy ESLint config file (in addition to existing flat config). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
.eslintrc.cjs
Outdated
| /* global module */ | ||
|
|
||
| module.exports = { | ||
| root: true, | ||
| ignorePatterns: [ | ||
| // These fixtures intentionally exercise AssemblyScript-only syntax that | ||
| // TypeScript's parser rejects before lint rules can run. | ||
| "tests/compiler/parameter-decorators.ts", | ||
| "tests/transform/parameter-decorators.ts" | ||
| ], | ||
| parser: "@typescript-eslint/parser", | ||
| plugins: [ |
There was a problem hiding this comment.
This PR adds a full legacy .eslintrc.cjs alongside the existing flat eslint.config.js configuration. With ESLint v10 (as pinned in package.json), eslint.config.js is the active config and .eslintrc.* is typically ignored unless users opt out of flat config. Consider removing this file or documenting why it’s needed to avoid confusing contributors with two independent ESLint configurations.
There was a problem hiding this comment.
Agree, why we need this new eslintrc file?
There was a problem hiding this comment.
Sorry, this was a mistake.
| range: Range | ||
| range: Range, | ||
| decorators: DecoratorNode[] | null = null | ||
| ): ParameterNode { |
There was a problem hiding this comment.
I think range should be always at end.
decorators: DecoratorNode[] | null,
range: Range,
): ParameterNode {And yes, this will lead to more refactorings
|
Copilot creates more chaos and noise than it actually provides in a useful review. Not worth it. |
It is quiet unstable, sometime is pretty good but sometime totally wrong. |
|
Copilot = outdated GPT-4 + telemetry & logging content to Microsoft + terrible post train + run on Azure Sometimes Copilot even took screenshots (but only for the desktop) In short, main goal is not helping you |
Extend parameter AST nodes to retain decorators, including explicit-this decorators on function signatures, without forcing a broad constructor API churn. Teach both parameter parsers to accept stacked decorators on regular, rest, explicit-this, constructor-property, function-type, and parenthesized-arrow parameters. Update the AST builder to serialize parameter decorators inline so parser fixtures can round-trip the new syntax faithfully. Add a focused parser fixture covering the preserved syntax surface before deferred validation is introduced.
Add a Program-owned validation pass that walks the final AST and reports TS1206 once per decorated parameter, using the full decorator-list span for the diagnostic range. Invoke that validation from compile after transforms have had their afterInitialize window, so transformers can still remove parameter decorators before any diagnostics are emitted. Add a dedicated compiler rejection fixture covering regular, rest, explicit-this, constructor-property, function-type, function-expression, and arrow-parameter cases.
Add a dedicated transform input containing the same invalid parameter-decorator forms exercised by the compiler rejection fixture. Introduce ESM and CommonJS afterInitialize transforms that walk the AST and strip parameter decorators, including explicit-this decorators on function signatures. Extend the transform test scripts to compile that input with the stripping transforms, proving no TS1206 diagnostics are emitted once transforms remove the decorators in time.
03925af to
c267d6e
Compare
| /** Rejects parameter decorators that survive transform time. These remain transform-only syntax. */ | ||
| validateParameterDecorators(): void { | ||
| if (this.parameterDecoratorsValidated) return; | ||
| this.parameterDecoratorsValidated = true; | ||
| let sources = this.sources; | ||
| for (let i = 0, k = sources.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInSource(sources[i]); | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInSource(source: Source): void { | ||
| let statements = source.statements; | ||
| for (let i = 0, k = statements.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInStatement(statements[i]); | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInStatements(statements: Statement[] | null): void { | ||
| if (statements) { | ||
| for (let i = 0, k = statements.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInStatement(statements[i]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInStatement(statement: Statement | null): void { | ||
| if (!statement) return; | ||
| switch (statement.kind) { | ||
| case NodeKind.Block: { | ||
| this.validateParameterDecoratorsInStatements((<BlockStatement>statement).statements); | ||
| break; | ||
| } | ||
| case NodeKind.ClassDeclaration: | ||
| case NodeKind.InterfaceDeclaration: { | ||
| this.validateParameterDecoratorsInClassDeclaration(<ClassDeclaration>statement); | ||
| break; | ||
| } | ||
| case NodeKind.Do: { | ||
| let doStatement = <DoStatement>statement; | ||
| this.validateParameterDecoratorsInStatement(doStatement.body); | ||
| this.validateParameterDecoratorsInExpression(doStatement.condition); | ||
| break; | ||
| } | ||
| case NodeKind.EnumDeclaration: { | ||
| this.validateParameterDecoratorsInEnumDeclaration(<EnumDeclaration>statement); | ||
| break; | ||
| } | ||
| case NodeKind.ExportDefault: { | ||
| this.validateParameterDecoratorsInDeclaration((<ExportDefaultStatement>statement).declaration); | ||
| break; | ||
| } | ||
| case NodeKind.Expression: { | ||
| this.validateParameterDecoratorsInExpression((<ExpressionStatement>statement).expression); | ||
| break; | ||
| } | ||
| case NodeKind.For: { | ||
| let forStatement = <ForStatement>statement; | ||
| this.validateParameterDecoratorsInStatement(forStatement.initializer); | ||
| this.validateParameterDecoratorsInExpression(forStatement.condition); | ||
| this.validateParameterDecoratorsInExpression(forStatement.incrementor); | ||
| this.validateParameterDecoratorsInStatement(forStatement.body); | ||
| break; | ||
| } | ||
| case NodeKind.ForOf: { | ||
| let forOfStatement = <ForOfStatement>statement; | ||
| this.validateParameterDecoratorsInStatement(forOfStatement.variable); | ||
| this.validateParameterDecoratorsInExpression(forOfStatement.iterable); | ||
| this.validateParameterDecoratorsInStatement(forOfStatement.body); | ||
| break; | ||
| } | ||
| case NodeKind.FunctionDeclaration: | ||
| case NodeKind.MethodDeclaration: { | ||
| this.validateParameterDecoratorsInFunctionDeclaration(<FunctionDeclaration>statement); | ||
| break; | ||
| } | ||
| case NodeKind.If: { | ||
| let ifStatement = <IfStatement>statement; | ||
| this.validateParameterDecoratorsInExpression(ifStatement.condition); | ||
| this.validateParameterDecoratorsInStatement(ifStatement.ifTrue); | ||
| this.validateParameterDecoratorsInStatement(ifStatement.ifFalse); | ||
| break; | ||
| } | ||
| case NodeKind.NamespaceDeclaration: { | ||
| this.validateParameterDecoratorsInStatements((<NamespaceDeclaration>statement).members); | ||
| break; | ||
| } | ||
| case NodeKind.Return: { | ||
| this.validateParameterDecoratorsInExpression((<ReturnStatement>statement).value); | ||
| break; | ||
| } | ||
| case NodeKind.Switch: { | ||
| let switchStatement = <SwitchStatement>statement; | ||
| this.validateParameterDecoratorsInExpression(switchStatement.condition); | ||
| let cases = switchStatement.cases; | ||
| for (let i = 0, k = cases.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInSwitchCase(cases[i]); | ||
| } | ||
| break; | ||
| } | ||
| case NodeKind.Throw: { | ||
| this.validateParameterDecoratorsInExpression((<ThrowStatement>statement).value); | ||
| break; | ||
| } | ||
| case NodeKind.Try: { | ||
| let tryStatement = <TryStatement>statement; | ||
| this.validateParameterDecoratorsInStatements(tryStatement.bodyStatements); | ||
| this.validateParameterDecoratorsInStatements(tryStatement.catchStatements); | ||
| this.validateParameterDecoratorsInStatements(tryStatement.finallyStatements); | ||
| break; | ||
| } | ||
| case NodeKind.TypeDeclaration: { | ||
| this.validateParameterDecoratorsInTypeDeclaration(<TypeDeclaration>statement); | ||
| break; | ||
| } | ||
| case NodeKind.Variable: { | ||
| let declarations = (<VariableStatement>statement).declarations; | ||
| for (let i = 0, k = declarations.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInVariableLikeDeclaration(declarations[i]); | ||
| } | ||
| break; | ||
| } | ||
| case NodeKind.Void: { | ||
| this.validateParameterDecoratorsInExpression((<VoidStatement>statement).expression); | ||
| break; | ||
| } | ||
| case NodeKind.While: { | ||
| let whileStatement = <WhileStatement>statement; | ||
| this.validateParameterDecoratorsInExpression(whileStatement.condition); | ||
| this.validateParameterDecoratorsInStatement(whileStatement.body); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInDeclaration(declaration: DeclarationStatement): void { | ||
| switch (declaration.kind) { | ||
| case NodeKind.ClassDeclaration: | ||
| case NodeKind.InterfaceDeclaration: { | ||
| this.validateParameterDecoratorsInClassDeclaration(<ClassDeclaration>declaration); | ||
| break; | ||
| } | ||
| case NodeKind.EnumDeclaration: { | ||
| this.validateParameterDecoratorsInEnumDeclaration(<EnumDeclaration>declaration); | ||
| break; | ||
| } | ||
| case NodeKind.FieldDeclaration: | ||
| case NodeKind.VariableDeclaration: { | ||
| this.validateParameterDecoratorsInVariableLikeDeclaration(<VariableLikeDeclarationStatement>declaration); | ||
| break; | ||
| } | ||
| case NodeKind.FunctionDeclaration: | ||
| case NodeKind.MethodDeclaration: { | ||
| this.validateParameterDecoratorsInFunctionDeclaration(<FunctionDeclaration>declaration); | ||
| break; | ||
| } | ||
| case NodeKind.NamespaceDeclaration: { | ||
| this.validateParameterDecoratorsInStatements((<NamespaceDeclaration>declaration).members); | ||
| break; | ||
| } | ||
| case NodeKind.TypeDeclaration: { | ||
| this.validateParameterDecoratorsInTypeDeclaration(<TypeDeclaration>declaration); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInClassDeclaration(node: ClassDeclaration): void { | ||
| this.validateParameterDecoratorsInTypeParameters(node.typeParameters); | ||
| this.validateParameterDecoratorsInType(node.extendsType); | ||
| let implementsTypes = node.implementsTypes; | ||
| if (implementsTypes) { | ||
| for (let i = 0, k = implementsTypes.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInType(implementsTypes[i]); | ||
| } | ||
| } | ||
| this.validateParameterDecoratorsInIndexSignature(node.indexSignature); | ||
| let members = node.members; | ||
| for (let i = 0, k = members.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInDeclaration(members[i]); | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInEnumDeclaration(node: EnumDeclaration): void { | ||
| let values = node.values; | ||
| for (let i = 0, k = values.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInVariableLikeDeclaration(values[i]); | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInFunctionDeclaration(node: FunctionDeclaration): void { | ||
| this.validateParameterDecoratorsInTypeParameters(node.typeParameters); | ||
| this.validateParameterDecoratorsInFunctionType(node.signature); | ||
| this.validateParameterDecoratorsInStatement(node.body); | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInTypeDeclaration(node: TypeDeclaration): void { | ||
| this.validateParameterDecoratorsInTypeParameters(node.typeParameters); | ||
| this.validateParameterDecoratorsInType(node.type); | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInVariableLikeDeclaration(node: VariableLikeDeclarationStatement): void { | ||
| this.validateParameterDecoratorsInType(node.type); | ||
| this.validateParameterDecoratorsInExpression(node.initializer); | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInSwitchCase(node: SwitchCase): void { | ||
| this.validateParameterDecoratorsInExpression(node.label); | ||
| this.validateParameterDecoratorsInStatements(node.statements); | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInIndexSignature(node: IndexSignatureNode | null): void { | ||
| if (!node) return; | ||
| this.validateParameterDecoratorsInType(node.keyType); | ||
| this.validateParameterDecoratorsInType(node.valueType); | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInExpression(node: Expression | null): void { | ||
| if (!node) return; | ||
| switch (node.kind) { | ||
| case NodeKind.Assertion: { | ||
| let assertion = <AssertionExpression>node; | ||
| this.validateParameterDecoratorsInExpression(assertion.expression); | ||
| this.validateParameterDecoratorsInType(assertion.toType); | ||
| break; | ||
| } | ||
| case NodeKind.Binary: { | ||
| let binary = <BinaryExpression>node; | ||
| this.validateParameterDecoratorsInExpression(binary.left); | ||
| this.validateParameterDecoratorsInExpression(binary.right); | ||
| break; | ||
| } | ||
| case NodeKind.Call: { | ||
| let call = <CallExpression>node; | ||
| this.validateParameterDecoratorsInExpression(call.expression); | ||
| this.validateParameterDecoratorsInTypes(call.typeArguments); | ||
| this.validateParameterDecoratorsInExpressions(call.args); | ||
| break; | ||
| } | ||
| case NodeKind.Class: { | ||
| this.validateParameterDecoratorsInClassDeclaration((<ClassExpression>node).declaration); | ||
| break; | ||
| } | ||
| case NodeKind.Comma: { | ||
| this.validateParameterDecoratorsInExpressions((<CommaExpression>node).expressions); | ||
| break; | ||
| } | ||
| case NodeKind.ElementAccess: { | ||
| let elementAccess = <ElementAccessExpression>node; | ||
| this.validateParameterDecoratorsInExpression(elementAccess.expression); | ||
| this.validateParameterDecoratorsInExpression(elementAccess.elementExpression); | ||
| break; | ||
| } | ||
| case NodeKind.Function: { | ||
| this.validateParameterDecoratorsInFunctionDeclaration((<FunctionExpression>node).declaration); | ||
| break; | ||
| } | ||
| case NodeKind.InstanceOf: { | ||
| let instanceOf = <InstanceOfExpression>node; | ||
| this.validateParameterDecoratorsInExpression(instanceOf.expression); | ||
| this.validateParameterDecoratorsInType(instanceOf.isType); | ||
| break; | ||
| } | ||
| case NodeKind.Literal: { | ||
| this.validateParameterDecoratorsInLiteral(<LiteralExpression>node); | ||
| break; | ||
| } | ||
| case NodeKind.New: { | ||
| let newExpression = <NewExpression>node; | ||
| this.validateParameterDecoratorsInTypes(newExpression.typeArguments); | ||
| this.validateParameterDecoratorsInExpressions(newExpression.args); | ||
| break; | ||
| } | ||
| case NodeKind.Parenthesized: { | ||
| this.validateParameterDecoratorsInExpression((<ParenthesizedExpression>node).expression); | ||
| break; | ||
| } | ||
| case NodeKind.PropertyAccess: { | ||
| this.validateParameterDecoratorsInExpression((<PropertyAccessExpression>node).expression); | ||
| break; | ||
| } | ||
| case NodeKind.Ternary: { | ||
| let ternary = <TernaryExpression>node; | ||
| this.validateParameterDecoratorsInExpression(ternary.condition); | ||
| this.validateParameterDecoratorsInExpression(ternary.ifThen); | ||
| this.validateParameterDecoratorsInExpression(ternary.ifElse); | ||
| break; | ||
| } | ||
| case NodeKind.UnaryPostfix: { | ||
| this.validateParameterDecoratorsInExpression((<UnaryPostfixExpression>node).operand); | ||
| break; | ||
| } | ||
| case NodeKind.UnaryPrefix: { | ||
| this.validateParameterDecoratorsInExpression((<UnaryPrefixExpression>node).operand); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInExpressions(nodes: Expression[] | null): void { | ||
| if (nodes) { | ||
| for (let i = 0, k = nodes.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInExpression(nodes[i]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInLiteral(node: LiteralExpression): void { | ||
| switch (node.literalKind) { | ||
| case LiteralKind.Array: { | ||
| this.validateParameterDecoratorsInExpressions((<ArrayLiteralExpression>node).elementExpressions); | ||
| break; | ||
| } | ||
| case LiteralKind.Object: { | ||
| this.validateParameterDecoratorsInExpressions((<ObjectLiteralExpression>node).values); | ||
| break; | ||
| } | ||
| case LiteralKind.Template: { | ||
| this.validateParameterDecoratorsInExpressions((<TemplateLiteralExpression>node).expressions); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInType(node: TypeNode | null): void { | ||
| if (!node) return; | ||
| switch (node.kind) { | ||
| case NodeKind.FunctionType: { | ||
| this.validateParameterDecoratorsInFunctionType(<FunctionTypeNode>node); | ||
| break; | ||
| } | ||
| case NodeKind.NamedType: { | ||
| this.validateParameterDecoratorsInTypes((<NamedTypeNode>node).typeArguments); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInTypes(nodes: TypeNode[] | null): void { | ||
| if (nodes) { | ||
| for (let i = 0, k = nodes.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInType(nodes[i]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInTypeParameters(nodes: TypeParameterNode[] | null): void { | ||
| if (nodes) { | ||
| for (let i = 0, k = nodes.length; i < k; ++i) { | ||
| let node = nodes[i]; | ||
| this.validateParameterDecoratorsInType(node.extendsType); | ||
| this.validateParameterDecoratorsInType(node.defaultType); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInFunctionType(node: FunctionTypeNode): void { | ||
| this.reportParameterDecorators(node.explicitThisDecorators); | ||
| this.validateParameterDecoratorsInType(node.explicitThisType); | ||
| let parameters = node.parameters; | ||
| for (let i = 0, k = parameters.length; i < k; ++i) { | ||
| this.validateParameterDecoratorsInParameter(parameters[i]); | ||
| } | ||
| this.validateParameterDecoratorsInType(node.returnType); | ||
| } | ||
|
|
||
| private validateParameterDecoratorsInParameter(node: ParameterNode): void { | ||
| this.reportParameterDecorators(node.decorators); | ||
| this.validateParameterDecoratorsInType(node.type); | ||
| this.validateParameterDecoratorsInExpression(node.initializer); | ||
| } | ||
|
|
||
| private reportParameterDecorators(decorators: DecoratorNode[] | null): void { | ||
| if (decorators && decorators.length > 0) { | ||
| this.error( | ||
| DiagnosticCode.Decorators_are_not_valid_here, | ||
| Range.join(decorators[0].range, decorators[decorators.length - 1].range) | ||
| ); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
This is quite a lot of code (almost a complete AST pass) for a relatively simple feature. There's probably a more compact and general approach that can take its place, like during an earlier pass maintaining a set of references to decorators (or decorator-containing parents if necessary), and after initalize walk just this set to see what remains. Otherwise, if we did it like this, we'd ultimately end up with a bunch of adhoc passes eventually, that all need to be updated whenever the AST changes.
There was a problem hiding this comment.
Btw I also thought it would be better to extend the existing methods for parsing decorators. We could add a boolean operator to indicate that it is indeed a parameter. Although we need to assess how much overlap there is. @jtenner have you considered that opportunity?
There was a problem hiding this comment.
Im unsure what @MaxGraey means, but I think this should be consolidated instead of added as a pass now that I look at it. Any ideas?
There was a problem hiding this comment.
Thanks, I took this route in 6f75b5e.
Instead of keeping a bespoke recursive pass in Program, I split the problem into three parts:
parser.tsrecords which source-level statements actually preserved parameter decorators while parsing,ast.tsowns subtree traversal through a small sharedNodeWalker, andprogram.tsnow just runs a tiny parameter-specific validator afterafterInitialize.
The main reason I chose this approach was to get AST topology knowledge out of Program and into one shared traversal utility, so we do not keep accumulating feature-specific recursive passes there as the AST evolves. That seemed like the best way to address the maintainability concern you raised while still keeping the transform timing/behavior the same.
If you would prefer this to validate by walking all sources after afterInitialize, or if there is a better place/shape for the walker, I am happy to adjust. Any feedback or suggestions would be appreciated.
There was a problem hiding this comment.
It is among the more usable fictions of the present moment that cognition itself can be cleanly externalized into an adjacent intelligence, though the very convenience of that displacement is precisely what renders it corrosive: one entrusts thought to the apparatus only to discover, in the flattening light of that delegation, the full contour of one's accumulated judgment already exposed, already spent, already standing outside itself.
Replace the bespoke parameter-decorator validation pass in Program with a small validator built on top of a shared AST walker. The previous implementation worked, but it reimplemented a near-complete AST traversal inside program.ts for one feature. That made Program responsible for AST topology, duplicated traversal logic, and created exactly the kind of ad hoc pass review feedback called out: every AST shape change would have to remember to update this validator as well. This change separates three concerns more cleanly: - parser.ts records which source-level statements actually preserved parameter decorators while parsing - ast.ts owns subtree traversal through a generic NodeWalker utility - program.ts only performs the parameter-specific check after afterInitialize In other words, the parser decides what roots are relevant, the walker decides how to descend the tree, and the validator decides what to report. This is better because it keeps AST traversal knowledge in one place, removes a large one-off recursive pass from Program, and makes future AST validation/analysis passes much smaller if they need to inspect subtrees later. The validator still runs after afterInitialize so transforms can inspect or remove preserved parameter decorators first, and the tests continue to cover parser round-tripping, compiler rejection, and transform-time removal. Also add a namespace regression case to ensure preserved parameter decorators nested under a tracked source-level statement are still validated and can still be stripped by transforms.
Drop AST imports from src/program.ts that became unused after moving subtree traversal into NodeWalker. This fixes the lint warnings reported in CI and keeps the parameter decorator refactor clean.
|
Follow-up: I fixed the lint warnings from the refactor by removing the now-unused AST imports in src/program.ts (53dc57c). I also reran full validation locally: npm run check, npm run build, and npm test all passed. |
Follow the existing AST convention that Range is the last constructor field and the last factory/helper parameter. This updates ParameterNode and Node.createParameter accordingly, and adjusts all call sites.
Account for explicit-this parameter decorators in the same way as parameter decorators: wire explicitThisDecorators through FunctionTypeNode and Node.createFunctionType, keep it adjacent to explicitThisType, and leave range as the last constructor/helper parameter. Update all call sites accordingly.
src/extra/ast.ts
Outdated
| private visitParameterDecorator(node: DecoratorNode): void { | ||
| let sb = this.sb; | ||
| sb.push("@"); | ||
| this.visitNode(node.name); | ||
| let args = node.args; | ||
| if (args) { | ||
| sb.push("("); | ||
| let numArgs = args.length; | ||
| if (numArgs) { | ||
| this.visitNode(args[0]); | ||
| for (let i = 1; i < numArgs; ++i) { | ||
| sb.push(", "); | ||
| this.visitNode(args[i]); | ||
| } | ||
| } | ||
| sb.push(")"); | ||
| } | ||
| sb.push(" "); | ||
| } |
There was a problem hiding this comment.
It's literally very similar to serializeDecorator. I suggest combining this such similar methods for other parts into a single one serializeDecorator (or visitDecorator), and accounting for the difference in logic with an optional second parameter
There was a problem hiding this comment.
By the way, thank you for explaining. It wasn't clear to me what you meant, and this was good feedback!
There was a problem hiding this comment.
I think there’s something similar somewhere else. Try to investigate more. Also, you could delegate this to LLM with good reasoning
Remove the duplicate parameter-decorator serializer in ASTBuilder and fold the inline formatting case into serializeDecorator with an optional flag. This keeps decorator serialization in one place while preserving the spacing differences between declaration decorators and parameter decorators.
| private parseParameterDecorators( | ||
| tn: Tokenizer | ||
| ): DecoratorNode[] | null { | ||
| // Preserve parameter decorators in the AST so transforms can inspect or remove them later. | ||
| let decorators: DecoratorNode[] | null = null; | ||
| while (tn.skip(Token.At)) { | ||
| let decorator = this.parseDecorator(tn); | ||
| if (!decorator) break; | ||
| if (!decorators) decorators = [decorator]; | ||
| else decorators.push(decorator); | ||
| } | ||
| return decorators; | ||
| } |
There was a problem hiding this comment.
For example, I think it's a good candidate
There was a problem hiding this comment.
A good candidate for what?
There was a problem hiding this comment.
It used only once. It could be inclined I guess. Or this already has some helper. I don't remember
There was a problem hiding this comment.
Can you make a suggestion please? I'm sorry I don't understand.
There was a problem hiding this comment.
parseParameterDecorators has only one usage and quite small. Why we need parseParameterDecorators function? Just manually inline it
src/parser.ts
Outdated
| } | ||
|
|
||
| /** Consumes decorators that appear after a parameter has already started. */ | ||
| private reportInvalidParameterDecorators(tn: Tokenizer): void { |
There was a problem hiding this comment.
| private reportInvalidParameterDecorators(tn: Tokenizer): void { | |
| private tryParseParameterDecorators(tn: Tokenizer): void { |
| // LogicalOr // a || b | ||
| } | ||
|
|
||
| class ParameterDecoratorValidator extends NodeWalker { |
There was a problem hiding this comment.
Why do we need a NodeWalker? And why we can't do validation after finishing parsing of decorators?
There was a problem hiding this comment.
If you do parsing of the decorators they need to survive to become transformed. If they exist after the transform phase, that is when validation should kick in.
Unless I misunderstand how things work?
There was a problem hiding this comment.
No, it has usually done via "error parser recovery" mechanics. As far as I can recall, AssemblyScript also follows this approach to some extent. The idea is that you carry on parsing: if you encounter an error, for example missed closing bracket, you're print the error and close bracket by yourself, then continue parsing
There was a problem hiding this comment.
@MaxGraey If there is a parsing error, does a transform still work?
There was a problem hiding this comment.
Why should transform work if we can't properly parse decorators?
As I already said, the parser is designed in such a way that it doesn't stop if errors occur. It tries to fix everything automatically and continue parsing. So, theoretically, you can pass incorrect AST to transformer for instance. But why?
There was a problem hiding this comment.
For context: The general idea behind the design is that one parse discovers all the parse errors in one go, instead of failing early, and only discovering more as one fixes the source. Is for diagnostics, not implying that when there is a parse error, the source is actually attempted to be compiled.
Rename the parser helper that consumes and reports decorators found after a parameter has already started from reportInvalidParameterDecorators to tryParseParameterDecorators, matching its behavior more closely and aligning with the review suggestion.
Summary
This PR makes parameter decorators parseable and preservable in the AST so transforms can inspect or remove them before compilation-time validation runs.
This is intentionally not a new end-user language feature. Any parameter decorators that survive transform time still produce
TS1206: Decorators are not valid here.What Changed
ParameterNodethisparameter decorators onFunctionTypeNodeProgramvalidation pass that rejects surviving parameter decorators once per decorated parameter using the full decorator-list rangeafterInitialize, so transforms can remove the decorators firstWhy
The parser and transform pipeline can use preserved parameter decorators as an AST-level extension point without committing AssemblyScript to supporting them as regular language syntax.
That gives transform authors a useful hook while keeping the language semantics unchanged:
Impact
TS1206if parameter decorators survive to compilationValidation
npm run buildnpm run test:parser -- parameter-decoratorsnpm run test:compiler -- parameter-decorators --noDiffnpm run test:transform